L’objectif de ce TP est d’illustrer les notions abordées pour la méthode DBSCAN et pour la clasisifcation hiérarchique. Les librairies R nécessaires pour ce TP :

## Pour faire le TP
library(mclust)
library(cluster)
library(clusterSim)
library(factoextra)
library(FactoMineR)
library(reticulate)
library(ggplot2)
library(reshape2)
library(circlize)
library(viridis)

Les librairies Python nécessaires pour ce TP :

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import plotly.express as px

from sklearn.cluster import DBSCAN
from sklearn.metrics.cluster import adjusted_rand_score
from sklearn.metrics import silhouette_samples, silhouette_score
from sklearn.neighbors import NearestNeighbors
from scipy.cluster.hierarchy import dendrogram, linkage
from sklearn.cluster import AgglomerativeClustering
from yellowbrick.cluster import KElbowVisualizer
from sklearn.cluster import KMeans

On reprend dans ce second TP les données wine disponibles sur la page moodle du cours. On charge ici les données.

wine<-read.table("wine.txt",header=T)
wine$Qualite = as.factor(wine$Qualite)
wine$Type = factor(wine$Type, labels = c("blanc", "rouge"))

wineinit<-wine
wine[,-c(1,2)]<-scale(wine[,-c(1,2)],center=T,scale=T)

head(wine)
##      Qualite  Type      AcidVol    AcidCitr     SO2lbr      SO2tot     Densite
## 1352  medium rouge  1.638714588 -1.92626362 -1.2083376 -1.15967786 -0.46497450
## 5493  medium blanc -0.068544417 -1.35617574 -0.7004747 -0.85707581 -0.33499781
## 5153  medium blanc -0.800226847 -0.59605856  0.5409681 -0.02047014  1.32391517
## 5308  medium blanc -0.007570881  0.92417581  1.7824108  1.27893867  1.08790487
## 3866  medium blanc  0.419243870  0.03737243 -0.5311870  0.99413674  0.03783006
## 694   medium rouge  0.785085086  0.03737243 -0.4747578  0.19313131  1.27260858
##           Alcool
## 1352  1.14546909
## 5493 -1.12092616
## 5153 -1.29526426
## 5308 -1.29526426
## 3866  0.09944051
## 694  -0.94658806

On fait une ACP pour la visualisation des résultats dans la suite

resacp<-PCA(wine,quali.sup=c(1,2), scale.unit = TRUE,graph=FALSE)

Et on prépare aussi pour Python

winepy=r.wine
winequant=winepy
winequant=winequant.drop(columns=['Type','Qualite'])
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler();
scaler.fit(winequant);
winequant=scaler.transform(winequant)

from sklearn.decomposition import PCA
mypca = PCA(n_components=3);
mypca.fit(winequant);
dataPCA=mypca.fit_transform(winequant)

1 Classification avec DBSCAN

1.1 Avec le logiciel R

Question : Dans un premier temps, utilisez l’algorithme DBSCAN avec les paramètres minPts= 7 et eps= 1 à l’aide de la fonction dbscan() de la librairie dbscan. Quels sont les effectifs par classe ? Combien d’individus ne sont pas classés ?

# A COMPLETER
minPts<-7
eps<-1
res.db <- dbscan::dbscan(...)
table(...)
fviz_cluster(res.db, wine[,-c(1:2)], geom="point",ellipse="FALSE")+
  theme(legend.position="none")+
  xlab("")+ylab("")+ggtitle("Avec DBSCAN")

Correction :

minPts<-7
eps<-1
res.db <- dbscan::dbscan(wine[,-c(1,2)],eps=eps,minPts=minPts)
table(res.db$cluster)
## 
##   0   1   2   3   4 
## 138 360  75  22   5
fviz_cluster(res.db, wine[,-c(1:2)], geom="point",ellipse="FALSE")+
  theme(legend.position="none")+
  xlab("")+ylab("")+ggtitle("Avec DBSCAN")

Question : Pour étudier l’influence des paramètres minPts et eps, évaluez le nombre de classes obtenues et le nombre d’individus non classés pour différentes valeurs de ces paramètres.

minPts <- ...
eps <- ...
NBCluster <- matrix(0,nrow=length(minPts),ncol=length(eps))
NBNonCl <-matrix(0,nrow=length(minPts),ncol=length(eps))
for (i in 1:length(minPts)){
  for (j in 1:length(eps)){
    res<-dbscan::dbscan(wine[,-c(1,2)], eps=eps[j], minPts=minPts[i])
    NBCluster[i,j] <- ...
    NBNonCl[i,j] <- ...
  }
}

df<-data.frame(eps=rep(eps,each=length(minPts)),
              minPts=as.factor(rep(minPts,length(eps))),
              NBCluster=c(NBCluster),
              NBNonCl=c(NBNonCl)*100/nrow(wine))

ggplot(df,aes(x=eps,y=NBCluster,col=minPts))+geom_point()+geom_line()
ggplot(df,aes(x=eps,y=NBNonCl,col=minPts))+geom_point()+geom_line()

Correction :

minPts <-seq(5,15,1)
eps <-seq(0.5,2,0.1)
NBCluster <- matrix(0,nrow=length(minPts),ncol=length(eps))
NBNonCl <-matrix(0,nrow=length(minPts),ncol=length(eps))
for (i in 1:length(minPts)){
  for (j in 1:length(eps)){
    res<-dbscan::dbscan(wine[,-c(1,2)], eps=eps[j], minPts=minPts[i])
    NBCluster[i,j] <- length(table(res$cluster))-1
    NBNonCl[i,j] <- sum(res$cluster==0)
  }
}

df<-data.frame(eps=rep(eps,each=length(minPts)),
               minPts=as.factor(rep(minPts,length(eps))),
               NBCluster=c(NBCluster),
               NBNonCl=c(NBNonCl)*100/nrow(wine))

ggplot(df,aes(x=eps,y=NBCluster,col=minPts))+geom_point()+geom_line()

ggplot(df,aes(x=eps,y=NBNonCl,col=minPts))+geom_point()+geom_line()

Question : Pour une valeur de minPts=7, tracez le graphe de distance kNN afin de choisir le paramètre eps. Vous pouvez utiliser la fonction kNNdistplot(). Qu’en pensez-vous ?

# A COMPLETER

Correction :

dbscan::kNNdistplot(wine[,-c(1,2)], k = 6)
abline(h=1.2)

res.db <- dbscan::dbscan(wine[,-c(1,2)],eps=1.2,minPts=7)
table(res.db$cluster)
## 
##   0   1 
##  72 528

1.2 Avec le logiciel Python

Question : Dans un premier temps, utilisez l’algorithme DBSCAN avec les paramètres minPts= 7 et eps= 1 à l’aide de la fonction DBSCAN de la librairie scikit-learn. Quels sont les effectifs par classe ? Combien d’individus ne sont pas classés ?

# A COMPLETER
from sklearn.cluster import DBSCAN
dbsc = DBSCAN(....).fit(....)
pd.crosstab(....,"freq")

import plotly.express as px
import pandas as pd
pca_df = pd.DataFrame({
    "Dim1" : dataPCA[:,0], 
    "Dim2" : dataPCA[:,1],
    "DBSCAN" : pd.Categorical(dbsc.labels_)
})

fig=px.scatter(pca_df,x="Dim1",y="Dim2",color="DBSCAN")
fig.show()

Correction :

from sklearn.cluster import DBSCAN
import seaborn as sns
dbsc = DBSCAN(eps=1, min_samples=7).fit(winequant)
pd.crosstab(dbsc.labels_,"freq")
## col_0  freq
## row_0      
## -1      138
##  0      360
##  1       75
##  2       22
##  3        5
import pandas as pd
pca_df = pd.DataFrame({
    "Dim1" : dataPCA[:,0], 
    "Dim2" : dataPCA[:,1],
    "DBSCAN" : pd.Categorical(dbsc.labels_)
})

fig=px.scatter(pca_df,x="Dim1",y="Dim2",color="DBSCAN")
fig.show()

#sns.lmplot( x="Dim1", y="Dim2", data=pca_df, fit_reg=False, hue='DBSCAN', legend=True);
#plt.show()

Question : Pour étudier l’influence des paramètres minPts et eps, évaluez le nombre de classes obtenues et le nombre d’individus non classés pour différentes valeurs de ces paramètres.

# A COMPLETER

epsvalue= ...
minPtsvalue=...
NBCluster = np.zeros((minPtsvalue.size,epsvalue.size))
NBNonCl=np.zeros((minPtsvalue.size,epsvalue.size))

for i in np.arange(0,minPtsvalue.size,1):
  for j in np.arange(0,epsvalue.size,1):
    resdbsc = DBSCAN(eps=...,min_samples=...).fit(...)
    NBCluster[i,j] = ...
    NBNonCl[i,j] = ...
    
# 
plt.plot(epsvalue,NBCluster[0,:], label='minPts=5', color='green')
plt.plot(epsvalue,NBCluster[4,:], label='minPts=9', color='steelblue')
plt.plot(epsvalue,NBCluster[7,:], label='minPts=12', color='purple')
plt.plot(epsvalue,NBCluster[10,:], label='minPts=15', color='red')
plt.legend()
plt.show()
plt.close()

#    
plt.plot(epsvalue,NBNonCl[0,:], label='minPts=5', color='green')
plt.plot(epsvalue,NBNonCl[4,:], label='minPts=9', color='steelblue')
plt.plot(epsvalue,NBNonCl[7,:], label='minPts=12', color='purple')
plt.plot(epsvalue,NBNonCl[10,:], label='minPts=15', color='red')
plt.legend()
plt.show()

Correction :

epsvalue=np.arange(0.5, 2.1, 0.1)
minPtsvalue=np.arange(5,16,1)
NBCluster = np.zeros((minPtsvalue.size,epsvalue.size))
NBNonCl=np.zeros((minPtsvalue.size,epsvalue.size))


for i in np.arange(0,minPtsvalue.size,1):
  for j in np.arange(0,epsvalue.size,1):
    resdbsc = DBSCAN(eps=epsvalue[j],min_samples=minPtsvalue[i]).fit(winequant)
    NBCluster[i,j] = max(resdbsc.labels_)+1
    NBNonCl[i,j] = (resdbsc.labels_==-1).sum()
    
# 
plt.plot(epsvalue,NBCluster[0,:], label='minPts=5', color='green')
plt.plot(epsvalue,NBCluster[4,:], label='minPts=9', color='steelblue')
plt.plot(epsvalue,NBCluster[7,:], label='minPts=12', color='purple')
plt.plot(epsvalue,NBCluster[10,:], label='minPts=15', color='red')
plt.legend()
plt.show()

plt.close()

#    
plt.plot(epsvalue,NBNonCl[0,:], label='minPts=5', color='green')
plt.plot(epsvalue,NBNonCl[4,:], label='minPts=9', color='steelblue')
plt.plot(epsvalue,NBNonCl[7,:], label='minPts=12', color='purple')
plt.plot(epsvalue,NBNonCl[10,:], label='minPts=15', color='red')
plt.legend()
plt.show()

Question : Pour une valeur de minPts=7, tracez le graphe de distance kNN afin de choisir le paramètre eps. Qu’en pensez-vous ?

# A COMPLETER
import numpy as np
from sklearn.neighbors import NearestNeighbors
neighbors = NearestNeighbors(.....)
neighbors_fit = neighbors.fit(.....)
distances, indices = neighbors_fit.kneighbors(......)
distancesmean = np.sort(distances.sum(axis=1)/6,axis=0)

plt.plot(distancesmean)
plt.axhline(y=....., linewidth=1, linestyle='dashed', color='k')
plt.ylabel("k-NN distance")
plt.xlabel("Points sorted by distanc)")
plt.show()

dbscopt = DBSCAN(eps=...., min_samples=....).fit(winequant)
pd.crosstab(dbscopt.labels_,"freq")

Correction :

import numpy as np
from sklearn.neighbors import NearestNeighbors
neighbors = NearestNeighbors(n_neighbors=6)
neighbors_fit = neighbors.fit(winequant)
distances, indices = neighbors_fit.kneighbors(winequant)
distancesmean = np.sort(distances.sum(axis=1)/6,axis=0)

plt.plot(distancesmean)
plt.axhline(y= 1 , linewidth=1, linestyle='dashed', color='k')
plt.ylabel("k-NN distance")
plt.xlabel("Points sorted by distance")
plt.show()

dbscopt = DBSCAN(eps=1, min_samples=7).fit(winequant)
pd.crosstab(dbscopt.labels_,"freq")
## col_0  freq
## row_0      
## -1      138
##  0      360
##  1       75
##  2       22
##  3        5

2 Classification hiérarchique

Dans cette section, nous nous intéressons à la classification hiérarchique que nous allons étudier avec R et python également.

2.1 Avec le logiciel R

Question : A l’aide de la fonction hclust, faites une classification hiérarchique des données de vins avec les mesures d’agrégation single, complete et average respectivement. Comparez visuellement les dendrogrammes associés. Commentez.

# A COMPLETER
hclustsingle<-hclust(...)
hclustcomplete<-hclust(...)
hclustaverage<-hclust(...)

# Dendrogramme
plot(hclustsingle,hang=-1,labels=FALSE)
...

fviz_dend(hclustsingle,show_labels=FALSE)
...

Correction :

Ajustement des trois classifications hiérarchiques à l’aide de la fonction hclust() :

d<-dist(wine[,-c(1,2)],method="euclidean")
hclustsingle<-hclust(d,method="single")
hclustcomplete<-hclust(d,method="complete")
hclustaverage<-hclust(d,method="average")

Visualisation des trois dendrogrammes :

#plot(hclustsingle,hang=-1,labels=FALSE)
#plot(hclustcomplete,hang=-1,labels=FALSE)
#plot(hclustaverage,hang=-1,labels=FALSE)

fviz_dend(hclustsingle,show_labels=FALSE)
## Warning: The `<scale>` argument of `guides()` cannot be `FALSE`. Use "none" instead as
## of ggplot2 3.3.4.
## ℹ The deprecated feature was likely used in the factoextra package.
##   Please report the issue at <]8;;https://github.com/kassambara/factoextra/issueshttps://github.com/kassambara/factoextra/issues]8;;>.

fviz_dend(hclustcomplete,show_labels=FALSE)

fviz_dend(hclustaverage,show_labels=FALSE)

Question : Déduisez du dendrogramme avec la mesure d’agrégation complete une classification en 3 classes. Vous pouvez utiliser la fonction cutree(). Comparez-la avec les variables Qualité et Type. Commentez.

# A COMPLETER

Correction :

ClassK3<-cutree(hclustcomplete,3)
table(ClassK3)
## ClassK3
##   1   2   3 
##  99 273 228
# Comparaison avec Qualité
table(ClassK3,wine$Qualite)
##        
## ClassK3 bad good medium
##       1   6   13     80
##       2   9   39    225
##       3   4   58    166
adjustedRandIndex(ClassK3,wine$Qualite)
## [1] 0.009529003
# Comparaison avec Type
table(ClassK3,wine$Type)
##        
## ClassK3 blanc rouge
##       1    20    79
##       2   178    95
##       3   227     1
adjustedRandIndex(ClassK3,wine$Type)
## [1] 0.2000887

Question : Tracez la distribution des variables quantitatives de wine en fonction de la classification en 3 classes de la question précédente. Commentez.

df<-data.frame(wine[,-c(1,2)],Class=as.factor(ClassK3))
df<-melt(df,id="Class")
ggplot(df,aes(x=variable,y=value))+geom_violin(aes(fill=Class))

Correction :

df<-data.frame(wine[,-c(1,2)],Class=as.factor(ClassK3))
df<-melt(df,id="Class")
ggplot(df,aes(x=variable,y=value))+geom_violin(aes(fill=Class))

Question : Dans cette question et pour les suivantes, on se focalise sur la mesure d’agrégation de Ward. Ajustez une classification hiérarchique avec la mesure de Ward. Que représentent les hauteurs du dendrogramme dans ce cas ?

# A COMPLETER
hward<-hclust(...)
fviz_dend(hward,show_labels=FALSE)

Correction :

hward<-hclust(d,method="ward.D2")
fviz_dend(hward,show_labels=FALSE)

Question : Déterminez le nombre de classes à retenir avec l’indice de Calinski_Harabasz. Vous pouvez vous aider de la fonction ìndex.G1() de la librairie clusterSim. Tracez la classification obtenue sur le dendrogramme et sur le premier plan factorielde l’ACP.

# A completer
ClustCH<-cutree(hward,....)
fviz_dend(hward,show_labels=FALSE,k=which.max(CH)+1)
fviz_pca_ind(resacp,geom = c("point"),habillage=as.factor(ClustCH))

Correction :

CH<-NULL
Kmax<-20
for (k in 2:Kmax){
  CH<-c(CH,index.G1(wine[,-c(1,2)],cutree(hward,k)))
}
daux<-data.frame(NbClust=2:Kmax,CH=CH)
ggplot(daux,aes(x=NbClust,y=CH))+geom_line()+geom_point()

ClustCH<-cutree(hward,which.max(CH)+1)
fviz_dend(hward,show_labels=FALSE,k=which.max(CH)+1)

fviz_pca_ind(resacp,geom = c("point"),habillage=as.factor(ClustCH))

table(ClustCH,wine$Qualite)
##        
## ClustCH bad good medium
##       1   5    7    122
##       2   5   36    193
##       3   6   55     91
##       4   3   12     65
table(ClustCH,wine$Type)
##        
## ClustCH blanc rouge
##       1    20   114
##       2   233     1
##       3   138    14
##       4    34    46

Question : Déterminez le nombre de classes à retenir avec le critère Silhouette. Vous pouvez vous aider de la fonction ìndex.S() de la librairie clusterSim. Comparez avec la classification de la question précédente.

# A COMPLETER

Correction :

Silhou<-NULL
Kmax<-20
for (k in 2:Kmax){
  Silhou<-c(Silhou,index.S(d,cutree(hward,k)))
}
daux<-data.frame(NbClust=2:Kmax,Silhouette=Silhou)
ggplot(daux,aes(x=NbClust,y=Silhouette))+geom_line()+geom_point()

Question : Comparez la classification obtenue avec la méthode des Kmeans dans le TP 1 et celle obtenue à la question précédente.

# A COMPLETER

Correction :

reskmeans<-kmeans(wine[,-c(1,2)],4)
table(reskmeans$cluster,cutree(hward,4))
##    
##       1   2   3   4
##   1  18   9  10  63
##   2 110   0   3   1
##   3   4 151   0  13
##   4   2  74 139   3
adjustedRandIndex(reskmeans$cluster,cutree(hward,4))
## [1] 0.4995215
library(circlize)
clust1F<-paste("ClKm-",reskmeans$cluster,sep="")
clust2F<-paste("ClCAH-",cutree(hward,4),sep="")
library(viridis)
mycolor <- viridis(8, alpha = 1, begin = 0, end = 1, option = "H")
mycolor <- mycolor[sample(1:8)]
chordDiagram(table(clust1F,clust2F),grid.col=mycolor)

2.2 Avec le logiciel python

Question : A l’aide de la fonction linkage, faites une classification hiérarchique des données de vins avec les mesures d’agrégation single, complete et average respectivement. Comparez visuellement les dendrogrammes associés. Commentez.

# A COMPLETER
from scipy.cluster.hierarchy import dendrogram, linkage
hsingle=linkage(...)
hcomplete=linkage(...)
haverage=linkage(...)

dendrogram(....,no_labels=True,color_threshold=0);
plt.show()

Correction :

Ajustement des trois classifications hiérarchiques à l’aide de la fonction hclust() :

from scipy.cluster.hierarchy import dendrogram, linkage
hsingle=linkage(winequant,method='single')
hcomplete=linkage(winequant,method='complete')
haverage=linkage(winequant,method='average')

Visualisation des trois dendrogrammes :

dendrogram(hsingle,no_labels=True,color_threshold=0);
plt.show()

dendrogram(hcomplete,no_labels=True,color_threshold=0);
plt.show()

dendrogram(haverage,no_labels=True,color_threshold=0);
plt.show()

Question : Déterminez une classification en 3 classes avec la mesure d’agrégation complete. Comparez-la avec les variables Qualité et Type. Commentez.

# A COMPLETER
from sklearn.cluster import AgglomerativeClustering
hierarchical_cluster = AgglomerativeClustering(.....)
ClustK3 = hierarchical_cluster.fit_predict(winequant)
# Comparer avec Qualite et Type
.....

Correction :

from sklearn.cluster import AgglomerativeClustering
hierarchical_cluster = AgglomerativeClustering(n_clusters=3, affinity='euclidean', linkage='complete')
ClustK3 = hierarchical_cluster.fit_predict(winequant)
pd.crosstab(ClustK3,"freq")
# Comparaison avec Qualité
## col_0  freq
## row_0      
## 0       228
## 1        99
## 2       273
pd.crosstab(ClustK3,winepy["Qualite"])
## Qualite  bad  good  medium
## row_0                     
## 0          4    58     166
## 1          6    13      80
## 2          9    39     225
adjusted_rand_score(ClustK3,winepy["Qualite"])

# Comparaison avec Type
## 0.009529003116147837
pd.crosstab(ClustK3,winepy["Type"])
## Type   blanc  rouge
## row_0              
## 0        227      1
## 1         20     79
## 2        178     95
adjusted_rand_score(ClustK3,winepy["Type"])
## 0.20008874452412492

Question : Tracez la distribution des variables quantitatives de wine en fonction de la classification en 3 classes de la question précédente. Commentez.

import plotly.express as px
aux=winepy.assign(Clust=ClustK3)
del aux["Qualite"]
del aux["Type"]
sm = aux.melt(id_vars='Clust')
fig=px.box(sm,x="Clust",y="value",facet_col="variable",facet_col_wrap=3,color="Clust")
fig.show()

Correction :

import plotly.express as px
aux=winepy.assign(Clust=ClustK3)
del aux["Qualite"]
del aux["Type"]
sm = aux.melt(id_vars='Clust')
fig=px.box(sm,x="Clust",y="value",facet_col="variable",facet_col_wrap=3,color="Clust")
fig.show()

Question : Dans cette question et pour les suivantes, on se focalise sur la mesure d’agrégation de Ward. Ajustez une classification hiérarchique avec la mesure de Ward. Que représentent les hauteurs du dendrogramme dans ce cas ?

# A COMPLETER

Correction :

hward=linkage(winequant,method='ward')
dendrogram(hward,no_labels=True,color_threshold=0);  
plt.show()

Question : Déterminez le nombre de classes à retenir avec l’indice de Calinski-Harabasz. Vous pouvez vous aider de la fonction KElbowVisualizer de la librairie yellowbrick. Tracez la classification obtenue sur le premier plan factoriel de l’ACP.

from yellowbrick.cluster import KElbowVisualizer
model = AgglomerativeClustering()
visualizer = KElbowVisualizer(......);
visualizer.fit(winequant)
visualizer.show()

Correction :

from yellowbrick.cluster import KElbowVisualizer
model = AgglomerativeClustering()
visualizer = KElbowVisualizer(model, k=(2,20), metric="calinski_harabasz",timings=False);
# Fit data to visualizer
visualizer.fit(winequant)
# Finalize and render figure
## KElbowVisualizer(ax=<AxesSubplot:>,
##                  estimator=AgglomerativeClustering(n_clusters=19), k=(2, 20),
##                  metric='calinski_harabasz', timings=False)
visualizer.show()
#visualizer.close()

modelfin = AgglomerativeClustering(n_clusters=4, affinity='euclidean', linkage='ward')
modelfin.fit(winequant)
## AgglomerativeClustering(n_clusters=4)
pd.crosstab(modelfin.labels_,"freq")
## col_0  freq
## row_0      
## 0       234
## 1       134
## 2       152
## 3        80
pca_df = pd.DataFrame({
    "Dim1" : dataPCA[:,0], 
    "Dim2" : dataPCA[:,1],
    "CAH" : pd.Categorical(modelfin.labels_)
})

fig=px.scatter(pca_df,x="Dim1", y="Dim2",color="CAH")
fig.show()

Question : Déterminez le nombre de classes à retenir avec le critère Silhouette. Vous pouvez vous aider de la fonction KElbowVisualizer de la librairie yellowbrick. Comparez avec la classification de la question précédente.

# A COMPLETER

Correction :

visualizer = KElbowVisualizer(model, k=(2,20), metric="silhouette",timings=False);
visualizer.fit(winequant);
visualizer.show()

Question : Comparez la classification obtenue avec la méthode des Kmeans dans le TP 1 et celle obtenue à la question précédente.

# A COMPLETER

Correction :

from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters = 4, init = 'k-means++', max_iter = 300, n_init = 10, random_state = 0)
kmeans.fit(winequant)
## KMeans(n_clusters=4, random_state=0)
modelfin2 = AgglomerativeClustering(n_clusters=4, affinity='euclidean', linkage='ward')
modelfin2.fit(winequant)
## AgglomerativeClustering(n_clusters=4)
pd.crosstab(modelfin2.labels_,kmeans.labels_)
## col_0    0    1   2    3
## row_0                   
## 0      153   74   7    0
## 1        4    2  18  110
## 2        0  139  10    3
## 3       13    3  63    1
adjusted_rand_score(modelfin2.labels_,kmeans.labels_)
## 0.5059827100331064